package jmine.tec.hydra.domain.actions;

import static jmine.tec.services.api.execution.ExecutorTransactionPolicy.PER_PAGE;
import static jmine.tec.services.api.execution.ExecutorValidationPolicy.BEFORE_BLOCK_EXECUTION;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;

import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Transient;

import jmine.tec.executionlog.ExecutionLogController;
import jmine.tec.executionlog.dao.ExecutionLogItemDAO;
import jmine.tec.executionlog.domain.ExecutionLog;
import jmine.tec.executionlog.domain.ExecutionLogItem;
import jmine.tec.executionlog.domain.enumx.ExecutionItemStatus;
import jmine.tec.hydra.HydraMessages;
import jmine.tec.hydra.api.DecodeMessageAction;
import jmine.tec.hydra.api.EncodeMessageAction;
import jmine.tec.hydra.api.FailedServicePageAction;
import jmine.tec.hydra.api.HydraAction;
import jmine.tec.hydra.api.HydraMessage;
import jmine.tec.hydra.api.SendMessageAction;
import jmine.tec.hydra.exception.HydraException;
import jmine.tec.hydra.script.DecodeResult;
import jmine.tec.hydra.util.stream.InputableOutputStream;
import jmine.tec.persist.annotation.DiscriminatorComment;
import jmine.tec.services.ServicesController;
import jmine.tec.services.api.Service;
import jmine.tec.services.api.ServiceExecutionException;
import jmine.tec.services.api.ServiceProperties;
import jmine.tec.services.api.ServicesPage;
import jmine.tec.services.api.execution.ExecutorParams;
import jmine.tec.services.api.factory.ServiceCreationException;
import jmine.tec.services.io.ServiceBean;
import jmine.tec.services.io.ServicesPageBean;
import jmine.tec.services.io.ServicesWriter;
import bancosys.tec.component.Documentation;
import bancosys.tec.persist.dao.BaseDAO;
import bancosys.tec.persist.dao.BeanNotFoundException;
import bancosys.tec.persist.dataset.HibernateRandomAccessResultSet;
import bancosys.tec.services.input.driver.XLSServiceWriter;

/**
 * Implementação padrão da interface DecodeMessageAction
 * 
 * @author lundberg
 */
@Entity
@DiscriminatorValue("2")
@DiscriminatorComment("DECODIFICACAO")
public class DefaultDecodeMessageAction extends AbstractHydraAction implements DecodeMessageAction {

    private ExecutionLog executionLog;

    // Transient
    private HydraMessage message;

    /**
     * {@inheritDoc}
     */
    public DecodeMessageAction prepare(HydraMessage msg) {
        this.message = msg;
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected List<HydraAction> doExecute() throws Exception {
        DecodeResult result = this.getRequest().getSpecification().getDecoder().decode(this.message);
        List<HydraAction> actions = new ArrayList<HydraAction>();
        for (String log : result.getLogs()) {
            this.logAsSystem(log);
        }
        if (!result.isSuccess()) {
            this.markAsPending();
        }
        if (result.hasServices()) {
            ExecutionLogController controller = this.getController().getExecutionLogController();
            InputStream input = this.servicesToInput(result);
            Long execLogId = controller.startExecution("HYDRA-SERVICES", this.message.getId() + ".xls", input).getId();
            List<ServicesPage> pages = null;
            try {
                pages = this.createServicesPages(result);
                this.executeServices(pages);
                controller.executionFinished();
                this.logAsSystem(String.format("%s serviços executados com sucesso.", this.findExecutionLog(execLogId).getTotalItems()));
            } catch (ServiceExecutionException e) {
                LOGGER.error("Erro ao executar página de serviços.", e);
                controller.executionFinished();
                actions.addAll(this.createFailedActions(pages, this.findExecutionLog(execLogId)));
            } catch (Exception e) {
                controller.addErrorItem("", e.getMessage(), e);
                controller.executionFinished();
                throw e;
            } catch (Error e) {
                controller.addErrorItem("", e.getMessage(), e);
                controller.executionFinished();
                throw e;
            } finally {
                this.executionLog = this.findExecutionLog(execLogId);
            }
        }
        if (result.hasMessages()) {
            for (Object src : result.getMessages()) {
                EncodeMessageAction action = this.getController().getProvider().createAction(EncodeMessageAction.class);
                action.prepare(src);
                actions.add(action);
            }
            this.logAsSystem(String.format("%s codificações de mensagens encaminhadas.", result.getMessages().size()));
        }
        if (result.shouldSend()) {
            SendMessageAction action = this.getController().getProvider().createAction(SendMessageAction.class);
            action.prepare(this.message);
            actions.add(action);
        }
        return actions;
    }

    /**
     * Encontra o execution log com o id dado
     * 
     * @param execLogId execLogId
     * @return ExecutionLog
     * @throws BeanNotFoundException BeanNotFoundException
     */
    private ExecutionLog findExecutionLog(Long execLogId) throws BeanNotFoundException {
        BaseDAO<ExecutionLog> dao = this.getController().getDAOFactory().getGenericDAO(ExecutionLog.class);
        return dao.findByPk(execLogId);
    }

    /**
     * Cria actions que registram que serviços falharam.
     * 
     * @param pages pages
     * @param execLog executionLog
     * @return List de HydraAction
     */
    private List<HydraAction> createFailedActions(List<ServicesPage> pages, ExecutionLog execLog) {
        Set<ExecutionItemStatus> failedStatus = new HashSet<ExecutionItemStatus>();
        failedStatus.add(ExecutionItemStatus.NOT_EXECUTED);
        failedStatus.add(ExecutionItemStatus.NOT_OK);
        ExecutionLogItemDAO itemDAO = this.getController().getDAOFactory().getDAOByClass(ExecutionLogItemDAO.class);
        HibernateRandomAccessResultSet<ExecutionLogItem> items = itemDAO.findByExecutionLog(execLog, null);
        int from = 0;
        int errors = 0;
        List<HydraAction> actions = new ArrayList<HydraAction>();
        for (ServicesPage page : pages) {
            if (page.getServices().isEmpty()) {
                continue;
            }
            int to = from + page.getServices().size();
            ExecutionLogItem execItemFrom = items.getObject(from);
            boolean failed = failedStatus.contains(execItemFrom.getStatus());
            if (failed) {
                errors += to - from;
                FailedServicePageAction action = this.getController().getProvider().createAction(FailedServicePageAction.class);
                action.prepare(execLog, execItemFrom, items.getObject(to - 1));
                actions.add(action);
            }
            from = to;
        }
        this.logAsSystem(String.format("%s serviços executados com sucesso, %s não executados.", itemDAO.countByExecutionLog(execLog)
                - errors, errors));
        return actions;
    }

    /**
     * Passa os serviços gerados para XML.
     * 
     * @param result result
     * @return InputStream
     * @throws IOException IOException
     */
    private InputStream servicesToInput(DecodeResult result) throws IOException {
        ServicesWriter servicesWriter = new XLSServiceWriter();
        InputableOutputStream output = new InputableOutputStream();
        servicesWriter.writeServices(result.getServices(), output);
        return output.getAsInputStream();
    }

    /**
     * Executa os serviços gerados
     * 
     * @param pages pages
     * @throws Exception Exception
     */
    private void executeServices(final List<ServicesPage> pages) throws Exception {
        this.getController().getPersistenceController().executeWithDisabledAuthorization(new Callable<Void>() {
            public Void call() throws Exception {
                DefaultDecodeMessageAction.this.getServicesController().getServicesPageExecutor()
                        .execute(pages, new ExecutorParams(PER_PAGE, BEFORE_BLOCK_EXECUTION, true));
                return null;
            }
        });
    }

    /**
     * Cria as páginas de serviço a partir do resultado do script de decodificação
     * 
     * @param result result
     * @return List de ServicesPage
     */
    private List<ServicesPage> createServicesPages(DecodeResult result) {
        List<ServicesPage> pages = new ArrayList<ServicesPage>(result.getServices().size());
        ServiceProperties parent = new ServiceProperties(this.getServicesController());
        for (ServicesPageBean bean : result.getServices()) {
            ServicesPage page = new ServicesPage();
            for (ServiceBean serviceBean : bean.getServices()) {
                Service<?> service = this.beanToService(serviceBean, parent);
                service.setServiceLocation(serviceBean.getServiceLocation());
                page.addService(service);
            }
            pages.add(page);
        }
        return pages;
    }

    /**
     * Instancia o serviço apropriado a partir do service bean dado.
     * 
     * @param bean bean
     * @param parent parent
     * @return Service
     */
    private Service<?> beanToService(ServiceBean bean, ServiceProperties parent) {
        Map<String, Object> parameters = this.getServicesController().instantiateMap();
        parameters.putAll(bean.getParameters());
        ServiceProperties serviceProperties = new ServiceProperties(this.getServicesController(), parent, parameters);
        try {
            return this.getServicesController().getServiceFactory().getService(bean.getAction(), bean.getName(), serviceProperties);
        } catch (ServiceCreationException e) {
            throw new HydraException(HydraMessages.ERRO_CRIACAO_SERVICO.create(bean.toString()), e);
        }
    }

    /**
     * {@inheritDoc}
     */
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "COD_EXECUTION_LOG")
    @Documentation("CODIGO DO EXECUTION LOG REFERENTE AOS SERVICOS RESULTANTES DA ACTION")
    public ExecutionLog getExecutionLog() {
        return this.executionLog;
    }

    /**
     * @param executionLog the executionLog to set
     */
    public void setExecutionLog(ExecutionLog executionLog) {
        this.executionLog = executionLog;
    }

    /**
     * Obtém o servicescontroller
     * 
     * @return ServicesController
     */
    @Transient
    private ServicesController getServicesController() {
        return this.getController().getServicesController();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected String createDescription() {
        return "Decodificação da mensagem";
    }
}
